package it.unibo.refolding.pico.ui;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.IntPair;
import it.unibo.refolding.pico.ApproachRetractionLoop;
import it.unibo.refolding.pico.PeakSlopes;
import it.unibo.refolding.pico.PeakSlopesHelper;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.shorts.ShortList;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.mutable.MutableInt;

import com.google.common.base.Preconditions;

public class PeakSlopesHolder implements DataHolder {

  private final Path file;
  // Raw data, not de-angled.
  private final ShortList z, def;
  final double contactPointZLsb;
  final int[] offsets;
  private List<PeakSlopes> peakSlopes;
  public IntList junctionPoints;

  /***
   * 
   * @param file
   * @param z
   * @param def
   * @param contactPointZLsb
   * @param offsets
   *          Each offset is relative to retractFrom(segment).
   * @param peakSlopes
   * @param junctionPoints
   */
  public PeakSlopesHolder(Path file, ShortList z, ShortList def, double contactPointZLsb,
    int[] offsets, List<PeakSlopes> peakSlopes, IntList junctionPoints) {
    Preconditions.checkArgument(!peakSlopes.isEmpty());
    Preconditions.checkArgument(z.size() == def.size());
    Preconditions.checkArgument(peakSlopes.size() == offsets.length);
    for (int i = 0; i < offsets.length; ++i) {
      int offset = offsets[i];
      PeakSlopes ps = peakSlopes.get(i);
      for (IntPair ip : ps) {
        Preconditions.checkArgument(ip.y < retractTo(i, junctionPoints)
          - retractFrom(i, junctionPoints) - offset);
      }
    }
    Preconditions.checkArgument(junctionPoints.size() == (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1)
      * peakSlopes.size());
    this.file = file;
    this.z = z;
    this.def = def;
    this.contactPointZLsb = contactPointZLsb;
    this.offsets = offsets;
    this.peakSlopes = new ArrayList<>(peakSlopes);
    this.junctionPoints = junctionPoints;
  }

  public IntList junctionPoints() {
    return junctionPoints;
  }

  public int loopCount() {
    return peakSlopes.size();
  }

  /**
   * @param relativeIndex
   *          must be relative to offset
   */
  public int removePeakSlopeCoveringIndex(int segment, int relativeIndex) {
    int i = peakSlopes.get(segment).peakIndex(relativeIndex);
    if (i != -1) {
      peakSlopes.set(segment, peakSlopes.get(segment).withoutPeakSlope(i));
    }
    return i;
  }

  public void removePeakSlopes(int segment, int[] o) {
    int[] c = Arrays.copyOf(o, o.length);
    Arrays.sort(c);
    for (int i = c.length; --i >= 1;) {
      Preconditions.checkArgument(c[i] > c[i - 1]);
    }
    for (int i = c.length; --i >= 0;) {
      peakSlopes.set(segment, peakSlopes.get(segment).withoutPeakSlope(c[i]));
    }
  }

  public int addPeakSlope(int segment, int relativeStartIndex, int relativePeakIndex) {
    Preconditions.checkArgument(relativeStartIndex <= relativePeakIndex);
    int[] o = overlapping(segment, relativeStartIndex, relativePeakIndex);
    if (o.length != 0) {
      return -1;
    }
    MutableInt insertionPoint = new MutableInt();
    peakSlopes.set(
      segment,
      peakSlopes.get(segment).withAddedPeakSlope(
        relativeStartIndex, relativePeakIndex, insertionPoint));
    return insertionPoint.intValue();
  }

  public int[] overlapping(int segment, int relativeStartIndex, int relativePeakIndex) {
    return peakSlopes.get(segment).overlapping(relativeStartIndex, relativePeakIndex);
  }

  public String filename() {
    return file.getFileName().toString();
  }

  @Override
  public String toString() {
    return ReflectionToStringBuilder.toString(this);
  }

  public void save() {
    try {
      PeakSlopesHelper.writeToFile(peakSlopes, file);
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public short minZ() {
    return CommonThings.min(z);
  }

  public short maxZ() {
    return CommonThings.max(z);
  }

  public short minDef() {
    return CommonThings.min(def);
  }

  public short maxDef() {
    return CommonThings.max(def);
  }

  public int offset(int segment) {
    return offsets[segment];
  }

  public ShortList sharedZ() {
    return z;
  }

  public ShortList sharedDef() {
    return def;
  }

  /**
   * @return the relative position of the first point with the given z and def
   *         values.
   */
  public int indexOfZDef(int segment, IntPair ip) {
    int from = retractFrom(segment);
    int to = retractTo(segment);
    int offset = offsets[segment];
    for (int i = from + offset; i < to; ++i) {
      if (z.getShort(i) == ip.x & def.getShort(i) == ip.y) {
        return i - (from + offset);
      }
    }
    return -1;
  }

  public int[] indicesOfZDef(int segment, IntPair ip) {
    int from = retractFrom(segment);
    int to = retractTo(segment);
    int offset = offsets[segment];
    IntList indices = new IntArrayList(1);
    for (int i = from + offset; i < to; ++i) {
      if (z.getShort(i) == ip.x & def.getShort(i) == ip.y) {
        indices.add(i - (from + offset));
      }
    }
    return indices.toIntArray();
  }

  public short zAtRelativeIndex(int segment, int i) {
    return z.getShort(retractFrom(segment) + offsets[segment] + i);
  }

  public short defAtRelativeIndex(int segment, int i) {
    return def.getShort(retractFrom(segment) + offsets[segment] + i);
  }

  @Override
  public int size() {
    return z.size();
  }

  public IntPair get(int segment, int i) {
    return peakSlopes.get(segment).get(i);
  }

  public int retractFrom(int segment) {
    return retractFrom(segment, junctionPoints);
  }

  private static int retractFrom(int segment, IntList junctionPoints) {
    return junctionPoints.getInt(segment
      * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1) + 2);
  }

  public int retractTo(int segment) {
    return retractTo(segment, junctionPoints);
  }

  public int retractTo(int segment, IntList junctionPoints) {
    return 1 + junctionPoints.getInt(segment
      * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1) + 3);
  }

  public PeakSlopes peakSlopes(int i) {
    return peakSlopes.get(i);
  }
}
